/*
Copyright (C) 2012 Brady Vidovic

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package ftr.tools;

import java.io.InputStream;
import btv.logger.BTVLogLevel;
import btv.io.BTVTailerListener;
import java.lang.ProcessBuilder.*;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Scanner;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.input.Tailer;
import static ftr.tools.Constants.*;

/**
 * 
 * @author Brady Vidovic
 */
public class TSJoiner implements Runnable
{        
    public static void main(String[] args) throws Exception
    {        
        Logger.setLogLevel(BTVLogLevel.DEBUG);
        TSJoiner j = new TSJoiner(new File("C:\\ffmpeg\\bin\\ffmpeg.exe"), new File("C:\\file_joined.ts"));
        for(File f : FileUtils.listFiles(new File("C:\\!tmpfiles"), null, false))
            j.addVideo(f);
        //j.addVideo(new File("C:\\file2.ts"));
        j.join();
    }
    
    public int timeoutSeconds = 45;//if no new lines are seen in log for this many seconds, will stop waiting and return false
    
    final File ffmpegExe;
    final File outFile, logFile;
    private List<File> videosToJoin;    
    private Thread ffmpegThread, timeoutThread, tailerThread;
    private BTVTailerListener btvTailingListener;
    private int tailSleepTime = 1000;
    private boolean timedOut = false;
    private Tailer tailer;
    public TSJoiner(File ffmpegExe, File outFile)
    {
        this.ffmpegExe = ffmpegExe;
        this.outFile = outFile;
        this.logFile = new File(BASE_DIR, "ffmpeg-join.log");                
        videosToJoin = new ArrayList<File>();
    }
    
    public void setTimeoutSeconds(int timeoutSeconds)
    {
        this.timeoutSeconds = timeoutSeconds;
    }
    
    public void addVideo(File mpgOrTsVideo)
    {
        videosToJoin.add(mpgOrTsVideo);
    }
    
    public boolean join() throws Exception
    {
        if(videosToJoin.isEmpty())
            throw new Exception("No videos found to join...");
        
        Logger.INFO("Using FFmpeg to join "+ videosToJoin.size() +" videos, output to: "+ outFile);                
        
        
        if(!ffmpegExe.isFile())
            throw new Exception("No file found at: "+ ffmpegExe.getAbsolutePath());
        
        
        if(outFile.isFile())//don't overwrite
        {
            if(TESTING) 
                outFile.delete();
            else
                throw new Exception("Destination file already exits: "+ outFile.getAbsolutePath());
        }
        
        //make sure they are all in the same base dir
        File workingDir = videosToJoin.get(0).getParentFile();
        for(File video : videosToJoin)
        {
            File nextWorkingDir = video.getParentFile();
            if(!workingDir.equals(nextWorkingDir))
                throw new Exception("Videos in different directories are not supported: "+ LINE_BRK+
                        workingDir+LINE_BRK+
                        nextWorkingDir);
        }
        
        //clear out log file
        try
        {
            if(logFile.isFile()) 
                if(!logFile.delete())
                    throw new IOException("Failed to delete existing log file: "+logFile);
                logFile.createNewFile();
        }
        catch(IOException iox)
        {
            Logger.ERROR("Failed to create new log file at: "+ logFile);
            return false;
        } 
        
        String concatParam = "concat:";
        for(Iterator<File> it = videosToJoin.iterator(); it.hasNext();)
        {
            File videoFile = it.next();            
            if(!videoFile.isFile())
                throw new Exception("No video file found at: "+ videoFile.getAbsolutePath());
            
            String ext = FilenameUtils.getExtension(videoFile.getName());
            if(!"ts".equalsIgnoreCase(ext) && !"mpg".equalsIgnoreCase(ext))
                throw new Exception("File to join must be ts or mpg. Found: "+ ext);
                        
            concatParam += videoFile.getName();//not absolute path since we are setting the working directory
            if(it.hasNext()) concatParam += "|";
        }
                
        Logger.DEBUG("concat param: "+ concatParam);                        
        
        //ffmpeg.exe -i concat:"file1.ts|file2.ts|file3.ts" -vcodec copy -acodec copy -f vob combined.ts                
        int exitCode = -1;
        try
        {
            ffmpegThread = Thread.currentThread();

            //tail the log            
            //need to write to log because FFmpeg does not write to stdout/stderr consistently
            Logger.INFO("Logging to: "+ logFile);
            btvTailingListener = new BTVTailerListener(BTVLogLevel.DEBUG);
            tailer = new Tailer(logFile, btvTailingListener, tailSleepTime);
            tailerThread = new Thread(tailer);
            tailerThread.start();                       
            Logger.DEBUG("Tailing logfile using waitInterval of: "+ tailSleepTime);

            //start timeout thread
            timeoutThread = new Thread(this);
            timeoutThread.start();

            String cmd = "\""+ffmpegExe+"\" -i \""+concatParam+"\" -vcodec copy -acodec copy -f vob \""+outFile.getAbsolutePath()+"\"";
            //cmd = "ping localhost -n 5";
            Logger.INFO("Executing (in working directory: "+ workingDir+")" + LINE_BRK + cmd);
            final Process pr = 
                Runtime.getRuntime().exec(
                    cmd,
                    null,
                    workingDir
                    );
            
            InputStream is = pr.getErrorStream();//ffmpeg writes to error stream by default
            
            //write to log file
            Scanner s = new Scanner(is);
            FileWriter fw = new FileWriter(logFile,true);
            while(s.hasNextLine())
            {                
                fw.write(s.nextLine()+LINE_BRK);
                fw.flush();
            }
            fw.close();
            s.close();                                                         

            try
            {
                exitCode = pr.waitFor();
                Thread.sleep(tailSleepTime);//give tailer time to finish getting all lines
                Logger.INFO("FFmpeg exited with code "+ exitCode);        
            }
            catch(InterruptedException x)
            {                
                Logger.INFO("Timed out ("+timeoutSeconds+" seconds) while waiting for FFmpeg. Will end join now.");
                exitCode = -1;
            }
        }
        catch(Exception x)
        {
            Logger.ERROR("Error using ffmpeg to join videos: "+x,x);            
        }
        finally
        {                        
            timedOut = true;//end timeout thread
                        
            if(tailer != null)
                tailer.stop();
                        
            if(timeoutThread != null && timeoutThread.isAlive())
                timeoutThread.interrupt();
            
            boolean success = exitCode == 0;//normal exit code indicates success
            if(!success)            
                Logger.WARN("FFMpeg failed (exitCode="+exitCode+"). Log is:" +LINE_BRK + getLog());                                                                       
            else
                Logger.INFO("FFMpeg finished normally with exitCode "+ exitCode);
            return success;
        }                
    }
    
    public String getLog()
    {
        StringBuilder log = new StringBuilder();
        for(String s : btvTailingListener.getLines())
            log.append(s).append(LINE_BRK);
        return log.toString();
        
    }
    
    //Timeout thread
    @Override
    public void run() 
    {                        
        long lastLineTime = System.currentTimeMillis();
        try
        {            
            while(!timedOut)
            {    
                try
                {
                    //no new lines, see if we have timed out
                    if(btvTailingListener.hasFoundLines())
                    {
                        //reset timeout when new lines are recieved
                        lastLineTime = btvTailingListener.getLastNewLineTime();
                    }

                    long elapsedTime = System.currentTimeMillis() - lastLineTime;
                    Logger.DEBUG("Time since last new line = "+ (elapsedTime/1000) +" seconds. Initialized = "+ btvTailingListener.hasFoundLines());                 
                    if(elapsedTime > (timeoutSeconds * 1000))
                    {//timed out
                        if(ffmpegThread != null && ffmpegThread.isAlive())
                            ffmpegThread.interrupt();//interrupts waitFor()
                        
                        throw new InterruptedException("Timed out ("+timeoutSeconds +" seconds)");
                    }
                    else
                    {
                        ///wait for more input until timeout is reached                                                                         
                        Thread.sleep(tailSleepTime);
                    }
                }
                catch(InterruptedException x)
                {                        
                    timedOut = true;
                    Logger.DEBUG("Ending timeout thread.");                                                    
                }
            }
        }
        catch(Exception x)
        {
            Logger.ERROR("Failed to tail FFMpeg log...",x);
        }
    }    
}